<template>
  <div v-bind="$attrs" class="ttt-countdown">
    <slot name="default" :time="{disTime,...disDate}">
      {{ countdownText }}
    </slot>
  </div>
</template>

<script setup lang="ts">
import { onBeforeUnmount, reactive, ref, watch } from 'vue'
import type { PropType } from 'vue'

const emits = defineEmits<{
  (e: 'timeout', val: null): void
}>()
const props = defineProps({
  // 倒计时时间
  time: {
    type: [String, Number] as PropType<string | number>,
    default: 0,
    validator: (val: string | number) => !Number.isNaN(Number(val))
  },
  // 结束时间
  endTime: {
    type: [String, Number, Date] as PropType<string | number | Date>,
    default: 0,
    validator: (val: string | number) => !Number.isNaN(Number(val))
  },
  // 格式化方式
  format: {
    type: String as PropType<string>,
    default: 'hh:mm:ss'
  },
  // 倒计时频率
  step: {
    type: [String, Number] as PropType<string | number>,
    default: 1000
  }
})

let endTimes: number = 0
let countdownTimer: ReturnType<typeof setTimeout>
let animation: ReturnType<typeof requestAnimationFrame>
const disTime = ref<number>(0)
const countdownText = ref<string>('')
const disDate = reactive({ d: 0, h: 0, m: 0, s: 0, ms: 0, hh: '0', mm: '0', ss: '0', mss: '0' })

// 初始化倒计时
const initCountdown = () => {
  if (Number.isNaN(endTimes) || disTime.value <= 0) return
  countdownTimer && clearTimeout(countdownTimer)
  countdownTimer = setTimeout(() => {
    disTime.value = endTimes - Date.now()
    if (disTime.value > 0) {
      initCountdown()
    } else {
      disTime.value = 0
      emits('timeout', null)
    }
    formatCountdown()
  }, Number(props.step))
}

const initCountdownRequest = () => {
  animation && cancelAnimationFrame(animation)
  disTime.value = endTimes - Date.now()
  if (disTime.value > 0) {
    initCountdown()
  } else {
    disTime.value = 0
    animation && cancelAnimationFrame(animation)
    emits('timeout', null)
    return
  }
  formatCountdown()
  animation = requestAnimationFrame(initCountdownRequest)
}

// 格式化倒计时
const formatCountdown = () => {
  const d = Math.floor(disTime.value / 1000 / 60 / 60 / 24).toString().padStart(2, '0')
  const hhMart = /d/.test(props.format) ? 24 : Infinity
  const hh = (Math.floor(disTime.value / 1000 / 60 / 60) % hhMart).toString().padStart(2, '0')
  const mm = (Math.floor(disTime.value / 1000 / 60) % 60).toString().padStart(2, '0')
  const ss = (Math.floor(disTime.value / 1000) % 60).toString().padStart(2, '0')
  const ms = Math.floor(disTime.value % 1000).toString().padStart(3, '0')
  const obj: { [key: string]: string } = {
    d,
    hh,
    mm,
    ss,
    ms
  }
  disDate.d = Number(d)
  disDate.h = Number(hh)
  disDate.m = Number(mm)
  disDate.s = Number(ss)
  disDate.ms = Number(ms)
  disDate.hh = hh
  disDate.mm = mm
  disDate.ss = ss
  disDate.mss = ms
  countdownText.value = props.format.replace(/(d)|(hh)|(mm)|(ss)|(ms)/g, (key: string) => obj[key])
}

// 开始
const startPlay = () => {
  countdownTimer && clearTimeout(countdownTimer)
  endTimes = Number(props.endTime) || (Number(props.time) + Date.now())
  disTime.value = Number(props.endTime) || Number(props.time)
  formatCountdown()
  if (disTime.value <= 0) return
  Number(props.step) > 60 ? initCountdown() : initCountdownRequest()
}

watch(() => props.time, () => {
  if (Number(props.time) >= 0) startPlay()
}, { immediate: true, deep: true })

watch(() => props.endTime, () => {
  if (Number(props.endTime) >= 0) startPlay()
}, { immediate: true, deep: true })

onBeforeUnmount(() => {
  countdownTimer && clearTimeout(countdownTimer)
})
</script>
